<?php

/**
 * AfroSoft ACL API
 *
 * @cond COMPLETE_DOCS
 *
 * @package DB
 *
 * @author AfroSoft
 * @version @build.version@ (Build @build.number@)
 * @license http://www.mozilla.org/MPL/MPL-1.1.txt MPL 1.1
 * @copyright Copyright (c) AfroSoft
 */

/* * **** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is the AfroSoft ACL API.
 *
 * The Initial Developer of the Original Code is AfroSoft.
 * Portions created by the Initial Developer are Copyright (C) 2011 the Initial
 * Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Xavier L. <xavier.l@afrosoft.tk>
 *
 * ***** END LICENSE BLOCK ***** */

require_once 'ACL_DB_Interface.php';

class ACL_DB_mysql extends ACL_DB_Interface {

    /**
     * This array holds the parameters for the current connection.
     */
    private $connectionParams = array(
        'host' => null,
        'port' => null,
        'user' => null,
        'pass' => null,
        'db' => null,
        'prefix' => null,
    );

    /**
     * The constructor sets the parameters and connects to the database.
     *
     * The values that can be set in the parameter are
     *
     * @param array $param
     */
    public function __construct(array $param) {
        $this->connectionParams = array_merge($this->connectionParams, $param);

        $dsn = 'mysql:host=' . $this->connectionParams['host'];
        if (!empty($this->connectionParams['port'])) {
            $dsn .= ';port=' . $this->connectionParams['port'];
        }
        $dsn .= ';dbname=' . $this->connectionParams['db'];

        try {
            $this->connection = new PDO($dsn, $this->connectionParams['user'], $this->connectionParams['pass']);
        } catch (Exception $e) {
            throw $e;
        }
        $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        // That is to be able to write better SQL. Sadly, it means nothing under MySQL 5.0.2, so that's the minimum MySQL requirement. Most people have 5.1 anyways.
        $this->connection->exec('SET sql_mode = \'ANSI,TRADITIONAL\'');
    }

    /**
     * The parameters used with this function are:
     * - user : the user ID linked to the permission needed
     * - group :  the group ID linked to the permission needed
     * - key : the item key for the permission needed
     * - explicit : a boolean. If set to true, the default permission will not be used.
     *
     * @param array $param
     * @return array
     */
    public function getPermission(array $param) {
        $param = array_merge(array(
            'user' => null,
            'group' => null,
            'key' => null,
            'explicit' => false,
                ), $param);

        if (empty($param['key'])) {
            throw new MissingRequiredArgumentException(array('key'));
        }
        if (empty($param['user']) && empty($param['group'])) {
            throw new MissingListRequiredArgumentException(array('user', 'group'));
        }

        $result = array(
            'permission' => null,
            'type' => null,
        );

        if (!empty($param['group'])) {
            $getGroupPerm = $this->connection->query('SELECT "permission" FROM ' . $this->connectionParams['prefix'] . 'group_permissions WHERE "group_id"=\'' . $param['group'] . '\' AND "key"=\'' . $param['key'] . '\' LIMIT 1');
            $getGroupPermData = $getGroupPerm->fetchAll();
            if (!empty($getGroupPermData)) {
                $result['type'] = 'group';
                if ($getGroupPermData[0]['permission'] == 0) {
                    $result['permission'] = false;
                } else if ($getGroupPermData[0]['permission'] == 1) {
                    $result['permission'] = true;
                }
            }
        }
        if (!empty($param['user'])) {
            $getUserPerm = $this->connection->query('SELECT "permission" FROM ' . $this->connectionParams['prefix'] . 'user_permissions WHERE "user_id"=\'' . $param['user'] . '\' AND "key"=\'' . $param['key'] . '\' LIMIT 1');
            $getUserPermData = $getUserPerm->fetchAll();
            if (!empty($getUserPermData)) {
                $result['type'] = 'user';
                if ($getUserPermData[0]['permission'] == 0) {
                    $result['permission'] = false;
                } else if ($getUserPermData[0]['permission'] == 1) {
                    $result['permission'] = true;
                }
            }
        }

        if (empty($result['type']) && $param['explicit'] != true) {
            $getDefaultPerm = $this->connection->query('SELECT "default"  FROM ' . $this->connectionParams['prefix'] . 'items WHERE "key"=\'' . $param['key'] . '\' LIMIT 1');
            $getDefaultPermData = $getDefaultPerm->fetchAll();
            if (!empty($getDefaultPermData)) {
                $result['type'] = 'default';
                if ($getDefaultPermData[0]['default'] == 0) {
                    $result['permission'] = false;
                } else if ($getDefaultPermData[0]['default'] == 1) {
                    $result['permission'] = true;
                }
            }
        }

        return $result;
    }

    /**
     * The parameters used with this function are:
     * - type : either 'user' or 'group'
     * - id : if set, the id of the user or group for which the permissions are 
     *  fetched
     *
     * @param array $param
     */
    public function getAllPermissions(array $param) {
        $param = array_merge(array(
            'type' => null,
            'id' => null,
                ), $param);

        if (empty($param['type'])) {
            throw new MissingRequiredArgumentException(array('type'));
        }

        switch ($param['type']) {
            case 'user':
                $query = 'SELECT "id", "user_id", "key", "permission" FROM ' . $this->connectionParams['prefix'] . 'user_permissions';
                if (!empty($param['user'])) {
                    $query .= ' WHERE "user_id"=\'' . $param['id'] . '\'';
                } else {
                    $query .= ' ORDER BY "user_id"';
                }
                $getAllUserPermission = $this->connection->query($query);
                return $getAllUserPermission->fetchAll(PDO::FETCH_ASSOC);
                break;
            case 'group':
                $query = 'SELECT "id", "group_id", "key", "permission" FROM ' . $this->connectionParams['prefix'] . 'group_permissions';
                if (!empty($param['group'])) {
                    $query .= ' WHERE "group_id"=\'' . $param['id'] . '\'';
                } else {
                    $query .= ' ORDER BY "group_id"';
                }
                $getAllGroupPermission = $this->connection->query($query);
                return $getAllGroupPermission->fetchAll(PDO::FETCH_ASSOC);
                break;
            default:
                throw new InvalidTypeArgumentException();
                break;
        }
    }

    /**
     * The parameters used with this function are:
     * - id : the ID of the user or group to which the permission should be 
     *  added
     * - type : either 'user' or 'group'
     * - key : the item key on which the permission should be set
     * - permission : the permission to set, in a form of a boolean (false =
     *  deny)
     *
     * @param array $param
     * @return bool
     */
    public function addPermission(array $param) {
        $param = array_merge(array(
            'id' => null,
            'type' => null,
            'key' => null,
            'permission' => null,
                ), $param);


        if (empty($param['id']) || empty($param['type']) || empty($param['key']) || !isset($param['permission'])) {
            throw new MissingRequiredArgumentException(array('id', 'type', 'key', 'permission'));
        }

        $param['permission'] = ($param['permission'] != true) ? 0 : 1;

        switch ($param['type']) {
            case 'user' :
                $count = $this->connection->query('SELECT COUNT(1) as "count" FROM ' . $this->connectionParams['prefix'] . 'user_permissions WHERE "user_id"=\'' . $param['id'] . '\' AND "key"=\'' . $param['key'] . '\'')->fetchAll();
                if ($count[0]['count'] != 0) {
                    throw new PermissionAlreadySetException;
                }
                if ($this->connection->exec('INSERT INTO ' . $this->connectionParams['prefix'] . 'user_permissions ("user_id", "key", "permission") VALUES(\'' . $param['id'] . '\', \'' . $param['key'] . '\', \'' . $param['permission'] . '\')') == 1) {
                    return true;
                }
                break;
            case 'group' :
                $count = $this->connection->query('SELECT COUNT(1) as "count" FROM ' . $this->connectionParams['prefix'] . 'group_permissions WHERE "group_id"=\'' . $param['id'] . '\' AND "key"=\'' . $param['key'] . '\'')->fetchAll();
                if ($count[0]['count'] != 0) {
                    throw new PermissionAlreadySetException;
                }
                if ($this->connection->exec('INSERT INTO ' . $this->connectionParams['prefix'] . 'group_permissions ("group_id", "key", "permission") VALUES(\'' . $param['id'] . '\', \'' . $param['key'] . '\', \'' . $param['permission'] . '\')') == 1) {
                    return true;
                }
                break;
            default:
                throw new InvalidTypeArgumentException();
                break;
        }
            return false;
    }

    /**
     * The parameters used with this function are:
     * - id : the ID of the user or group whose permission should be modified
     * - type : either 'user' or 'group'
     * - key : the item key on which the permission should modified
     * - new_id : the modification of the ID
     * - new_key : the modification of the key
     * - new_permission :  the modification of the permission
     *
     * Only one of the parameters with the 'new_' prefix must be specified.
     *
     * @param array $param
     * @return bool
     */
    public function editPermission(array $param) {
        $param = array_merge(array(
            'id' => null,
            'type' => null,
            'key' => null,
            'new_id' => null,
            'new_key' => null,
            'new_permission' => null,
                ), $param);

        if (empty($param['id']) || empty($param['type']) || empty($param['key'])) {
            throw new MissingRequiredArgumentException(array('id', 'type', 'key'));
        }
        if ((empty($param['new_id'])) && empty($param['new_key']) && !isset($param['new_permission'])) {
            throw new MissingListRequiredArgumentException(array('new_id', 'new_key', 'new_permission'));
        }

        $param['new_permission'] = ($param['new_permission'] != true) ? 0 : 1;
           
        switch ($param['type']) {
            case 'user' :
                $count = $this->connection->query('SELECT COUNT(1) as "count" FROM ' . $this->connectionParams['prefix'] . 'user_permissions WHERE "user_id"=\'' . $param['id'] . '\' AND "key"=\'' . $param['key'] . '\'')->fetchAll();
                if ($count[0]['count'] == 0) {
                    throw new PermissionNotSetException;
                }
                $query = 'UPDATE ' . $this->connectionParams['prefix'] . 'user_permissions';
                $set = array();
                if (!empty($param['new_user'])) {
                    $set[] = ' "user_id"=\'' . $param['new_id'] . '\'';
                }
                if (!empty($param['new_key'])) {
                    $set[] = ' "key"=\'' . $param['new_key'] . '\'';
                }
                if (!empty($param['new_permission'])) {
                    $set[] = ' "permission"=\'' . $param['new_permission'] . '\'';
                }
                $query .= ' SET' . implode(',', $set) . ' WHERE "user_id"=\'' . $param['id'] . '\' AND "key"=\'' . $param['key'] . '\'';
                break;
            case 'group' :
                $count = $this->connection->query('SELECT COUNT(1) as "count" FROM ' . $this->connectionParams['prefix'] . 'group_permissions WHERE "group_id"=\'' . $param['id'] . '\' AND "key"=\'' . $param['key'] . '\'')->fetchAll();
                if ($count[0]['count'] == 0) {
                    throw new PermissionNotSetException;
                }
                $query = 'UPDATE ' . $this->connectionParams['prefix'] . 'group_permissions';
                $set = array();
                if (!empty($param['new_group'])) {
                    $set[] = ' "group_id"=\'' . $param['new_id'] . '\'';
                }
                if (!empty($param['new_key'])) {
                    $set[] = ' "key"=\'' . $param['new_key'] . '\'';
                }
                if (!empty($param['new_permission'])) {
                    $set[] = ' "permission"=\'' . $param['new_permission'] . '\'';
                }
                $query .= ' SET' . implode(',', $set) . ' WHERE "group_id"=\'' . $param['id'] . '\' AND "key"=\'' . $param['key'] . '\'';
                break;
            default:
                throw new InvalidTypeArgumentException();
                break;
        }
        
        if ($this->connection->exec($query) == 1) {
            return true;
        }
        return false;
    }

    /**
     * This functions removes custom permission associated with a key and a 
     * user or group.
     * 
     * The parameters used with this function are:
     * - id : the ID of the user or group from which to remove the permission
     * - type : either 'user' or 'group'
     * - key : the key associated with permission to remove
     *
     * @param array $param
     * @return bool
     */
    public function revertPermission(array $param) {
        $param = array_merge(array(
            'id' => null,
            'type' => null,
            'key' => null,
                ), $param);

        if (empty($param['user']) || empty($param['type']) || empty($param['key'])) {
            throw new MissingRequiredArgumentException(array('id', 'type', 'key'));
        }

        switch($param['type']) {
            case 'user':
                if ($this->connection->exec('DELETE FROM ' . $this->connectionParams['prefix'] . 'user_permissions WHERE "user_id"=\'' . $param['id'] . '\' AND "key"=\'' . $param['key'] . '\'') == 1) {
                    return true;
                }
                break;
            case 'group':
                if ($this->connection->exec('DELETE FROM ' . $this->connectionParams['prefix'] . 'group_permissions WHERE "group_id"=\'' . $param['id'] . '\' AND "key"=\'' . $param['key'] . '\'') == 1) {
                    return true;
                }
                break;
            default:
                throw new InvalidTypeArgumentException();
                break;
        }
        return false;
    }

    /**
     * The parameters used with this function are:
     * - key : the key for which to retrieve the record.
     *
     * @param array $param
     * @return array
     */
    public function getItem(array $param) {
        $param = array_merge(array(
            'key' => null,
                ), $param);

        if (empty($param['key'])) {
            throw new MissingRequiredArgumentException(array('key'));
        }

        $getItemData = $this->connection->query('SELECT "key", "name", "desc", "default" FROM ' . $this->connectionParams['prefix'] . 'items WHERE "key"=\'' . $param['key'] . '\' LIMIT 1');
        if ($getItemData->rowCount() != 1) {
            throw new ItemNotDefinedException;
        }

        $temp = $getItemData->fetchAll(PDO::FETCH_ASSOC);
        return $temp[0];
    }

    /**
     * There are no parameters accepted by this function.
     *
     * @return array
     */
    public function getAllItems() {
        $getAllItemData = $this->connection->query('SELECT "key", "name", "desc", "default" FROM ' . $this->connectionParams['prefix'] . 'items');
        return $getAllItemData->fetchAll(PDO::FETCH_ASSOC);
    }

    /**
     * The parameters used with this function are:
     * - key : the key of the item to add
     * - name : the human-readable name for the item to add
     * - desc : the description of the item to add
     * - default : the default permission to set, in a form of a boolean
     * (false = deny)
     *
     * @return bool
     */
    public function addItem(array $param) {
        $param = array_merge(array(
            'key' => null,
            'name' => null,
            'desc' => null,
            'default' => null,
                ), $param);

        if (empty($param['key']) || empty($param['name']) || empty($param['desc']) || !isset($param['default'])) {
            throw new MissingRequiredArgumentException(array('key', 'name', 'desc', 'default'));
        }

        $param['default'] = ($param['default'] != true) ? 0 : 1;

        $count = $this->connection->query('SELECT COUNT(1) as "count" FROM ' . $this->connectionParams['prefix'] . 'items WHERE "key"=\'' . $param['key'] . '\'')->fetchAll();
        if ($count[0]['count'] != 0) {
            throw new ItemAlreadyDefinedException($param['key']);
        }
        if ($this->connection->exec('INSERT INTO ' . $this->connectionParams['prefix'] . 'items ("key", "name", "desc", "default") VALUES(\'' . $param['key'] . '\', \'' . $param['name'] . '\', \'' . $param['desc'] . '\', \'' . $param['default'] . '\')') == 1) {
            return true;
        }
        return false;
    }

    /**
     * The parameters used with this function are:
     * - key : the item key for which the record should be modified
     * - new_key : the modification of the key
     * - new_name : the modification of the name
     * - new_desc : the modification of the description
     * - new_default :  the modification of the default permission
     *
     * Only one of the parameters with the 'new_' prefix must be specified.
     *
     * @param array $param
     * @return bool
     */
    public function editItem(array $param) {
        $param = array_merge(array(
            'key' => null,
            'new_key' => null,
            'new_name' => null,
            'new_desc' => null,
            'new_default' => null,
                ), $param);

        if (empty($param['key'])) {
            throw new MissingRequiredArgumentException(array('key'));
        }
        if (empty($param['new_key']) && empty($param['new_name']) && empty($param['new_desc']) && !isset($param['new_default'])) {
            throw new MissingListRequiredArgumentException(array('new_key', 'new_name', 'new_desc', 'new_default'));
        }

        $param['new_default'] = ($param['new_default'] != true) ? 0 : 1;

        $count = $this->connection->query('SELECT COUNT(1) as "count" FROM ' . $this->connectionParams['prefix'] . 'items WHERE "key"=\'' . $param['key'] . '\'')->fetchAll();
        if ($count[0]['count'] == 0) {
            throw new ItemNotDefinedException;
        }
        $query = 'UPDATE ' . $this->connectionParams['prefix'] . 'items';
        $set = array();
        if (!empty($param['new_key'])) {
            $set[] = ' "key"=\'' . $param['new_key'] . '\'';
        }
        if (!empty($param['new_name'])) {
            $set[] = ' "name"=\'' . $param['new_name'] . '\'';
        }
        if (!empty($param['new_desc'])) {
            $set[] = ' "desc"=\'' . $param['new_desc'] . '\'';
        }
        if (!empty($param['new_default'])) {
            $set[] = ' "default"=\'' . $param['new_default'] . '\'';
        }
        $query .= ' SET' . implode(',', $set) . ' WHERE "key"=\'' . $param['key'] . '\'';

        if ($this->connection->exec($query) == 1) {
            return true;
        }
        return false;
    }

    /**
     * The parameters used by this function are:
     * - key : the key for which the record should be removed.
     *
     * @param array $param
     * @return bool
     */
    public function removeItem(array $param) {
        $param = array_merge(array(
            'key' => null,
        ), $param);

        if (empty($param['key'])) {
            throw new MissingRequiredArgumentException(array('key'));
        }

        if ($this->connection->exec('DELETE FROM ' . $this->connectionParams['prefix'] . 'items WHERE "key"=\'' . $param['key'] . '\'') == 1) {
            return true;
        }
        return false;
    }

}

/** @endcond */
